
using Boilen.Guards;
using Boilen.Primitives.CodeGeneration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Xunit;
using Xunit.Extensions;


namespace Boilen.Primitives.Implementers {

    public class TestImmutableProperty : TestImplementers {

        [Theory]
        [PropertyData( "SourceTypes" )]
        public void code_compiles_for_1_ImmutableProperty( Type sourceType ) {
            var pci = MemberTestInfo.Create<int>( "intProperty" );

            var pt = Partial.Type( sourceType )
                .AddImmutableProperty<int>( pci.ParameterName, pci.Description )
                ;
            var members = Compile.PartialType( pt );

            CheckImmutableProperties( members, pci );
            CheckImmutableConstructors( members, pci );
        }

        [Theory]
        [PropertyData( "SourceTypes" )]
        public void code_compiles_for_1_ImmutableProperty_with_default_value( Type sourceType ) {
            var pci = MemberTestInfo.Create<bool>( "intProperty", true );

            var pt = Partial.Type( sourceType )
                .AddImmutableProperty<bool>( pci.ParameterName, pci.Description, p => p
                    .SetDefaultValue( pci.DefaultValue )
                )
                ;
            var members = Compile.PartialType( pt );

            CheckImmutableProperties( members, pci );
            CheckImmutableConstructors( members, pci );
        }

        [Theory]
        [PropertyData( "SourceTypes" )]
        public void code_compiles_for_1_ImmutableProperty_with_guard( Type sourceType ) {
            var pci = MemberTestInfo.Create<int>( "intProperty" );

            var pt = Partial.Type( sourceType )
                .AddImmutableProperty<int>( pci.ParameterName, pci.Description, p => p
                    .NotNull( null )
                )
                ;
            var members = Compile.PartialType( pt );

            CheckImmutableProperties( members, pci );
            var constructors = CheckImmutableConstructors( members, pci );

            int intPropertyValue = 0;
            var fullConstructor = constructors.Single( );
            object instance = TestGuards.UseContext(
                ( ) => fullConstructor.Invoke( new object[] { intPropertyValue } ),
                context => Assert.True( context.NotNullCalled )
            );
        }


        [Theory]
        [PropertyData( "SourceTypes" )]
        public void ImmutableProperty_constructor_uses_assigned_value( Type sourceType ) {
            var pci = MemberTestInfo.Create<int>( "intProperty" );

            var pt = Partial.Type( sourceType )
                .AddImmutableProperty<int>( pci.ParameterName, pci.Description )
                ;
            var members = Compile.PartialType( pt );

            var properties = CheckImmutableProperties( members, pci );
            var constructors = CheckImmutableConstructors( members, pci );

            int intPropertyValue = 12;
            var property = properties.Single( );
            var fullConstructor = constructors.Single( );
            object instance = fullConstructor.Invoke( new object[] { intPropertyValue } );
            int assignedPropertyValue = Assert.IsType<int>( property.GetValue( instance, null ) );
            Assert.Equal( assignedPropertyValue, intPropertyValue );
        }

        [Theory]
        [PropertyData( "SourceTypes" )]
        public void ImmutableProperty_with_default_value_full_constructor_uses_assigned_value( Type sourceType ) {
            var pci = MemberTestInfo.Create<int>( "intProperty", 1 );

            var pt = Partial.Type( sourceType )
                .AddImmutableProperty<int>( pci.ParameterName, pci.Description, p => p
                    .SetDefaultValue( pci.DefaultValue )
                )
                ;
            var members = Compile.PartialType( pt );

            var properties = CheckImmutableProperties( members, pci );
            var constructors = CheckImmutableConstructors( members, pci );

            int intPropertyValue = 12;
            var property = properties.Single( );
            var fullConstructor = constructors[0];
            object instance = fullConstructor.Invoke( new object[] { intPropertyValue } );
            int assignedPropertyValue = Assert.IsType<int>( property.GetValue( instance, null ) );
            Assert.Equal( assignedPropertyValue, intPropertyValue );
        }

        [Theory]
        [PropertyData( "SourceTypes" )]
        public void ImmutableProperty_with_default_value_partial_constructor_uses_default_value( Type sourceType ) {
            var pci = MemberTestInfo.Create<int>( "intProperty", 1 );
            var pciBuffer = MemberTestInfo.Create<string>( "bufferProperty" );

            var pt = Partial.Type( sourceType )
                .AddImmutableProperty<int>( pci.ParameterName, pci.Description, p => p
                    .SetDefaultValue( pci.DefaultValue )
                )
                .AddImmutableProperty<string>( pciBuffer.ParameterName, pciBuffer.Description )
                ;
            var members = Compile.PartialType( pt );

            var properties = CheckImmutableProperties( members, pci, pciBuffer );
            var constructors = CheckImmutableConstructors( members, pci, pciBuffer );

            string bufferPropertyValue = "buffer";
            var property = properties.Single( p => p.Name == pci.MemberName );
            var partialConstructor = constructors[1];
            object instance = partialConstructor.Invoke( new object[] { bufferPropertyValue } );
            int assignedPropertyValue = Assert.IsType<int>( property.GetValue( instance, null ) );
            Assert.Equal( assignedPropertyValue, pci.DefaultValue );
        }


        [Theory]
        [PropertyData( "SourceTypes" )]
        public void code_compiles_for_2_ImmutableProperties( Type sourceType ) {
            var pci1 = MemberTestInfo.Create<int>( "intProperty" );
            var pci2 = MemberTestInfo.Create<string>( "stringProperty" );

            var pt = Partial.Type( sourceType )
                .AddImmutableProperty<int>( pci1.ParameterName, pci1.Description )
                .AddImmutableProperty<string>( pci2.ParameterName, pci2.Description )
                ;
            var members = Compile.PartialType( pt );

            CheckImmutableProperties( members, pci1, pci2 );
            CheckImmutableConstructors( members, pci1, pci2 );
        }

        [Theory]
        [PropertyData( "SourceTypes" )]
        public void code_compiles_for_2_ImmutableProperties_with_1_default_value( Type sourceType ) {
            var pci1 = MemberTestInfo.Create<int>( "intProperty", 0 );
            var pci2 = MemberTestInfo.Create<string>( "stringProperty" );

            var pt = Partial.Type( sourceType )
                .AddImmutableProperty<int>( pci1.ParameterName, pci1.Description, p => p
                    .SetDefaultValue( pci1.DefaultValue )
                )
                .AddImmutableProperty<string>( pci2.ParameterName, pci2.Description )
                ;
            var members = Compile.PartialType( pt );

            CheckImmutableProperties( members, pci1, pci2 );
            CheckImmutableConstructors( members, pci1, pci2 );
        }

        [Theory]
        [PropertyData( "SourceTypes" )]
        public void code_compiles_for_3_ImmutableProperties_with_1_default_value_and_1_calculated_value( Type sourceType ) {
            var pci1 = MemberTestInfo.Create<int>( "required" );
            var pci2 = MemberTestInfo.Create<int>( "optional", 2 );
            var pci3 = MemberTestInfo.Create<int>( "calculated", 3 );

            var pt = Partial.Type( sourceType )
                .AddImmutableProperty<int>( pci1.ParameterName, pci1.Description )
                .AddImmutableProperty<int>( pci2.ParameterName, pci1.Description, p => p
                    .SetDefaultValue( pci2.DefaultValue )
                )
                .AddImmutableProperty<int>( pci3.ParameterName, pci3.Description, p => p
                    .SetIncludeInConstructor( false )
                    .SetDefaultValue( pci3.DefaultValue )
                )
                ;
            var members = Compile.PartialType( pt );

            CheckImmutableProperties( members, pci1, pci2, pci3 );
            CheckImmutableConstructors( members, pci1, pci2 );
        }

        [Theory]
        [PropertyData( "SourceTypes" )]
        public void code_compiles_for_2_ImmutableProperties_with_1_guard( Type sourceType ) {
            var pci1 = MemberTestInfo.Create<int>( "intProperty" );
            var pci2 = MemberTestInfo.Create<string>( "stringProperty", "default" );

            var pt = Partial.Type( sourceType )
                .AddImmutableProperty<int>( pci1.ParameterName, pci1.Description )
                .AddImmutableProperty<string>( pci2.ParameterName, pci2.Description, p => p
                    .NotNull( null )
                    .SetDefaultValue( pci2.DefaultValue )
                )
                ;
            var members = Compile.PartialType( pt );

            CheckImmutableProperties( members, pci1, pci2 );
            var constructors = CheckImmutableConstructors( members, pci1, pci2 );

            int intPropertyValue = 0;
            string stringPropertyValue = "string";
            var fullConstructor = constructors[0];
            var partialConstructor = constructors[1];
            object fullInstance = TestGuards.UseContext(
                ( ) => fullConstructor.Invoke( new object[] { intPropertyValue, stringPropertyValue } ),
                context => Assert.True( context.NotNullCalled )
            );
            object partialInstance = TestGuards.UseContext(
                ( ) => partialConstructor.Invoke( new object[] { intPropertyValue } ),
                context => Assert.True( context.NotNullCalled )
            );
        }

        [Theory]
        [PropertyData( "SourceTypes" )]
        public void code_compiles_for_2_ImmutableProperties_with_1_guard_with_external_documentation( Type sourceType ) {
            var pci1 = MemberTestInfo.Create<int>( "intProperty" );
            var pci2 = MemberTestInfo.Create<string>( "stringProperty", "default" );

            using( ChangeExternalDocumentationPrefix( ) ) {
                var pt = Partial.Type( sourceType )
                    .AddImmutableProperty<int>( pci1.ParameterName )
                    .AddImmutableProperty<string>( pci2.ParameterName, p => p
                        .NotNull( null )
                        .SetDefaultValue( pci2.DefaultValue )
                    )
                    ;
                var members = Compile.PartialType( pt );

                CheckImmutableProperties( members, pci1, pci2 );
            }
        }


        [Theory]
        [InlineData( typeof( BaseClass ) )]
        public void code_compiles_for_1_ImmutableProperty_on_derived_class( Type sourceType ) {
            var pci = MemberTestInfo.Create<int>( "intProperty", -1 );
            var required = MemberTestInfo.Create<int>( "requiredProperty" );
            var optional = MemberTestInfo.Create<int>( "optionalProperty" );

            var pt = Partial.Type( "DerivedClass", sourceType )
                .AddImmutableProperty<int>( pci.ParameterName, pci.Description, p => p
                    .SetDefaultValue( pci.DefaultValue )
                )
                ;
            var members = Compile.PartialType( pt );
        }


        [Theory]
        [InlineData( typeof( SourceStruct<> ) )]
        public void code_compiles_for_1_ImmutableProperty_on_generic_struct( Type sourceType ) {
            var pci = MemberTestInfo.Create<int>( "intProperty", -1 );

            var pt = Partial.Type( sourceType )
                .AddImmutableProperty<int>( pci.ParameterName, pci.Description, p => p
                    .SetDefaultValue( pci.DefaultValue )
                )
                ;
            var members = Compile.PartialType( pt );
        }

        [Theory]
        [InlineData( typeof( SourceStruct<> ) )]
        public void code_compiles_for_1_ImmutableProperty_on_generic_struct_with_external_documentation( Type sourceType ) {
            var pci = MemberTestInfo.Create<int>( "intProperty", -1 );

            using( ChangeExternalDocumentationPrefix( ) ) {
                var pt = Partial.Type( sourceType )
                    .AddImmutableProperty<int>( pci.ParameterName, p => p
                        .SetDefaultValue( pci.DefaultValue )
                    )
                    ;
                var members = Compile.PartialType( pt );
            }
        }


        #region Utility

        public static IEnumerable<object[]> SourceTypes { get { return SourceTypesCore; } }

        private static PropertyInfo[] CheckImmutableProperties( IEnumerable<MemberInfo> members, params MemberTestInfo[] propInfos ) {
            return CheckProperties( members, propInfos, property => {
                Assert.True( property.CanRead );
                Assert.False( property.CanWrite );
            } );
        }

        private static ConstructorInfo[] CheckImmutableConstructors( IEnumerable<MemberInfo> members, params MemberTestInfo[] propInfos ) {
            Type declaringType = members.First( ).DeclaringType;
            bool isStruct = declaringType.IsValueType;

            var partialProperties = propInfos.Where( p => !p.HasDefaultValue ).ToArray( );
            int partialParamterCount = partialProperties.Length;
            int fullParameterCount = propInfos.Length;

            bool anyDefaultValues = partialParamterCount != fullParameterCount;
            bool hasPartialConstructor = anyDefaultValues && (!isStruct || partialParamterCount > 0);
            int expectedConstructorCount = hasPartialConstructor ? 2 : 1;


            var constructors = CheckConstructors( members, expectedConstructorCount, propInfos );

            var fullConstructor = constructors[0];
            var fullParameters = fullConstructor.GetParameters( );
            CheckParameters( fullParameters, propInfos );

            if( hasPartialConstructor ) {
                var partialConstructor = constructors[1];
                var partialParameters = partialConstructor.GetParameters( );
                CheckParameters( partialParameters, partialProperties );
            }


            return constructors;
        }

        #endregion

    }

}
